#define _GNU_SOURCE
#include <stdio.h>
#include <assert.h>
#include <math.h>

#include <object_table.h>
#include <ellipse.h>
#include <line.h>
#include <circle.h>
#include <utils.h>
#include <debug.h>


/**
 * Allocate a new ellipse.
 *
 * @return the new allocated ellipse object.
 */
ellipse_t *
ellipse_new( void )
{
  ellipse_t *ellipse = calloc( 1, sizeof(*ellipse) );

  OBJECT_TYPE( ellipse ) = OBJECT_TYPE_ELLIPSE;

  return ellipse;
}

/**
 * Free memory allocated to ellipse.
 *
 * @param an allocated ellipse object.
 */
void
ellipse_free( ellipse_t *ellipse )
{
  assert( ellipse != NULL );
  free( ellipse );
}

/**
 * Return the string representation of this ellipse object.
 *
 * @note string is malloc()ated, please free() it later.
 *
 * @param ellipse the object to act on.
 *
 * @return string representation.
 */
char *
ellipse_str( ellipse_t *ellipse )
{
  char *s = NULL;
  int r;

  if ( ellipse == NULL )
    r = asprintf( &s, "Ellipse is NULL!" );
  else
    r = asprintf( &s,
		  "Ellipse( depth=%d, color=0x%08x, transp=%0.3f, "
		  "center=( %d, %d ), radiusx=%d, radiuxy=%d )",
		  OBJECT_DEPTH( ellipse ),
		  COLOR_TO_ARGB( OBJECT_COLOR( ellipse ) ),
		  OBJECT_TRANSP( ellipse ),
		  ellipse->center.x, ellipse->center.y,
		  ellipse->radiusx, ellipse->radiusy
		  );
  if ( r < 0 )
    s = NULL;

  return s;
}


/**
 * Draw pixel in four octant, be carefull to do not paint outside the screen
 */
static inline void
draw_pixel_four_octants( uint16_t x0, uint16_t y0, uint16_t ox, uint16_t oy,
			 surface_t *surface, color_t color, float transp )
{
  surface_draw_pixel( surface, x0 - ox, y0 - oy, color, transp );
  surface_draw_pixel( surface, x0 + ox, y0 + oy, color, transp );
  if ( ( oy != 0 ) && ( ox != 0 ) )
    {
      surface_draw_pixel( surface, x0 + ox, y0 - oy, color, transp );
      surface_draw_pixel( surface, x0 - ox, y0 + oy, color, transp );
    }
}

static inline void
draw_pixel_four_octants_antialias_in( uint16_t x0, uint16_t y0,
				      float ox, float oy,
				      surface_t *surface, color_t color,
				      float transp )
{
  float a = oy - trunc( oy );
  float b = ox - trunc( ox );
  transp = transp + a + b;

  oy = trunc( oy );
  ox = trunc( ox );

  surface_draw_pixel( surface, x0 - ox, y0 - oy, color, transp );
  surface_draw_pixel( surface, x0 + ox, y0 + oy, color, transp );
  if ( ( oy != 0 ) && ( ox != 0 ) )
    {
      surface_draw_pixel( surface, x0 + ox, y0 - oy, color, transp );
      surface_draw_pixel( surface, x0 - ox, y0 + oy, color, transp );
    }
}

static inline void
draw_pixel_four_octants_antialias_out( uint16_t x0, uint16_t y0,
				       float ox, float oy,
				       surface_t *surface, color_t color,
				       float transp )
{
  float a = oy - trunc( oy );
  float b = ox - trunc( ox );
  transp = transp + 1 - a - b;

  oy = trunc( oy );
  ox = trunc( ox );

  if ( b > 0.00001 )
    ox += 1;
  else
    oy += 1;

  surface_draw_pixel( surface, x0 - ox, y0 - oy, color, transp );
  surface_draw_pixel( surface, x0 + ox, y0 + oy, color, transp );
  if ( ( oy != 0 ) && ( ox != 0 ) )
    {
      surface_draw_pixel( surface, x0 + ox, y0 - oy, color, transp );
      surface_draw_pixel( surface, x0 - ox, y0 + oy, color, transp );
    }
}


static inline void
fill_ellipse_segments( uint16_t x0, uint16_t y0, uint16_t ox, uint16_t oy,
		       surface_t *surface, color_t color, float transp,
		       uint16_t last_oy )
{
  uint16_t nox = min( ox, x0 );
  uint16_t noy = min( oy, y0 );

  if ( last_oy != oy )
    {
      line_draw_horizontal( x0 - nox, x0 + ox, y0 - noy,
			    surface, color, transp);

      if ( oy != 0 )
	line_draw_horizontal( x0 - nox, x0 + ox, y0 + oy,
			      surface, color, transp);
    }
}


void
ellipse_draw_trivial( uint16_t x, uint16_t y,
		      uint16_t radiusx, uint16_t radiusy,
		      surface_t *surface, color_t color,
		      float transp, int fill )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );

  float radiusx2 = radiusx * radiusx;
  float radiusy2 = radiusy * radiusy;
  float radiusxy2 = radiusx2 * radiusy2;

  uint16_t last_oy=-1, uninit=-1;
  float oy, ox;

  for ( ox=0; ox <= radiusx; ox++ )
    {
      oy = sqrt( radiusxy2 - radiusy2 * ox * ox ) / (float)radiusx;

      if ( ( last_oy != uninit ) && ( last_oy - 1 > (int)oy ) )
	break;

      if ( fill )
	fill_ellipse_segments( x, y, ox, oy, surface, color, transp, last_oy );
      else
	draw_pixel_four_octants( x, y, ox, oy, surface, color, transp );

      last_oy = oy;
    }

  if ( last_oy != uninit )
    oy = last_oy;
  else
    oy = 0;

  for (; oy >= 0; oy-- )
    {
      ox = sqrt( radiusxy2 - radiusx2 * oy * oy ) / radiusy;

      if ( fill )
	fill_ellipse_segments( x, y, ox, oy, surface, color, transp, last_oy );
      else
	draw_pixel_four_octants( x, y, ox, oy, surface, color, transp );
    }
}

void
ellipse_draw_trivial_antialias( uint16_t x, uint16_t y,
				uint16_t radiusx, uint16_t radiusy,
				surface_t *surface, color_t color,
				float transp, int fill )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );

  float radiusx2 = radiusx * radiusx;
  float radiusy2 = radiusy * radiusy;
  float radiusxy2 = radiusx2 * radiusy2;

  uint16_t last_oy=-1, uninit=-1;
  float oy, ox;

  for ( ox=0; ox <= radiusx; ox++ )
    {
      oy = sqrt( radiusxy2 - radiusy2 * ox * ox ) / (float)radiusx;

      if ( ( last_oy != uninit ) && ( last_oy - 1 > (int)oy ) )
	break;

      draw_pixel_four_octants_antialias_out( x, y, ox, oy,
					     surface, color, transp );
      if ( fill )
	{
	  fill_ellipse_segments( x, y, ox, oy, surface, color, transp,
				 last_oy );
	  draw_pixel_four_octants( x, y, ox, oy, surface, color, transp );
	}
      else
	draw_pixel_four_octants_antialias_in( x, y, ox, oy,
					      surface, color, transp );

      last_oy = oy;
    }

  if ( last_oy != uninit )
    oy = last_oy;
  else
    oy = 0;

  for (; oy >= 0; oy-- )
    {
      ox = sqrt( radiusxy2 - radiusx2 * oy * oy ) / (float)radiusy;

      draw_pixel_four_octants_antialias_out( x, y, ox, oy,
					     surface, color, transp );
      if ( fill )
	{
	  fill_ellipse_segments( x, y, ox, oy, surface, color, transp,
				 last_oy );
	  draw_pixel_four_octants( x, y, ox, oy, surface, color, transp );
	}
      else
	draw_pixel_four_octants_antialias_in( x, y, ox, oy,
					      surface, color, transp );

    }
}


/**
 * Internal function to draw ellipses using the Bresenham (integer only)
 * algorithm.
 */
void
ellipse_draw_bresenham( uint16_t x, uint16_t y,
			uint16_t radiusx, uint16_t radiusy,
			surface_t *surface, color_t color,
			float transp, int fill )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );

  int32_t S, T;
  uint16_t ox, oy, last_oy=-1;
  uint32_t radiusx2, radiusy2;

  ox = 0;
  oy = radiusy;

  radiusx2 = radiusx * radiusx;
  radiusy2 = radiusy * radiusy;

  S = radiusx2 * ( 1 - 2 * radiusy ) + 2 * radiusy2;
  T = radiusy2 - 2 * radiusx2 * ( 2 * radiusy - 1 );

  if ( ! fill )
    draw_pixel_four_octants( x, y, ox, oy, surface, color, transp );
  while ( oy > 0 )
    {
      if ( S < 0 )
	{
	  S += 2 * radiusy2 * ( 2 * ox + 3 );
	  T += 4 * radiusy2 * ( ox + 1 );
	  ox++;
	}
      else if ( T < 0 )
	{
	  S += 2 * radiusy2 * ( 2 * ox + 3 ) - 4 * radiusx2 * ( oy - 1 );
	  T += 4 * radiusy2 * ( ox + 1 ) - 2 * radiusx2 * ( 2 * oy - 3 );
	  ox++;
	  oy--;
	}
      else
	{
	  S -= 4 * radiusx2 * ( oy - 1 );
	  T -= 2 * radiusx2 * ( 2 * oy - 3 );
	  oy--;
	}

      if ( fill )
	{
	  fill_ellipse_segments( x, y, ox, oy, surface, color, transp,
				 last_oy );
	  last_oy = oy;
	}
      else
	draw_pixel_four_octants( x, y, ox, oy, surface, color, transp );
    }
}





void (*ellipse_draw_aliased)( uint16_t, uint16_t, uint16_t, uint16_t,
			      surface_t *, color_t, float, int ) =
  ellipse_draw_bresenham;

void (*ellipse_draw_antialiased)( uint16_t, uint16_t, uint16_t,
				  uint16_t, surface_t *, color_t, float, int )=
  ellipse_draw_trivial_antialias;

/**
 * Generic ellipse drawing algoritm.
 *
 * You may toggle drawing using ellipse_draw_aliased and
 * ellipse_draw_antialiased variables.
 *
 * @param ellipse the ellipse object to draw.
 * @param surface where to draw the object.
 */
void
__ellipse_draw( ellipse_t *ellipse, surface_t *surface )
{
  assert( surface != NULL );
  assert( surface->buffer != NULL );

  color_t color    = OBJECT_COLOR( ellipse );
  float transp     = OBJECT_TRANSP( ellipse );
  coord_t c        = ellipse->center;
  uint16_t radiusx = ellipse->radiusx;
  uint16_t radiusy = ellipse->radiusy;

  int fill = OBJECT_DRAW_OPTS( ellipse ) & DRAW_FILL;

  void (*draw)( uint16_t, uint16_t, uint16_t, uint16_t, surface_t *,
		color_t, float, int ) = NULL;

  if ( radiusx == radiusy )
    {
      circle_t *circle = circle_new();
      object_init( OBJECT( circle ),
		   OBJECT_COLOR( ellipse ),
		   OBJECT_TRANSP( ellipse ),
		   OBJECT_DEPTH( ellipse ),
		   OBJECT_DRAW_OPTS( ellipse ) );
      circle->radius = radiusx;
      circle->center = c;
      __circle_draw( circle, surface );
      object_free( OBJECT( circle ) );
      return;
    }
  else if ( radiusx == 0 )
    {
      uint16_t y0 = ( c.y < radiusy ) ? 0 : ( c.y - radiusy );
      line_draw_vertical( c.x, y0, c.y + radiusy, surface, color, transp );
      return;
    }
  else if ( radiusy == 0 )
    {
      uint16_t x0 = ( c.x < radiusx ) ? 0 : ( c.x - radiusx );
      line_draw_horizontal( x0, c.x + radiusx, c.y, surface, color, transp );
      return;
    }


  if ( OBJECT_DRAW_OPTS( ellipse ) & DRAW_ANTIALIAS )
    draw = ellipse_draw_antialiased;
  else
    draw = ellipse_draw_aliased;

  draw( c.x, c.y, radiusx, radiusy, surface, color, transp, fill );
}

void
ellipse_draw( ellipse_t *ellipse, surface_t *surface )
{
  surface_reset_visits( surface );
  __ellipse_draw( ellipse, surface );
}
